Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
88 commits
Select commit Hold shift + click to select a range
76f7936
define protocol security domains and trust boundaries
the-code-learner Aug 18, 2026
8e21490
add canonical protocol security domains
the-code-learner Aug 18, 2026
cc9e077
bind transactions to chain domains and canonical identity
the-code-learner Aug 18, 2026
c9064be
make protocol chain identity explicit
the-code-learner Aug 18, 2026
dc8c417
require explicit transaction chain and canonical signatures
the-code-learner Aug 18, 2026
f5d0918
require chain-bound consensus signing domains
the-code-learner Aug 18, 2026
3b72257
add request-bound replay-safe peer authentication
the-code-learner Aug 18, 2026
cf00703
bind validator identities and consensus signing to chain domains
the-code-learner Aug 18, 2026
508e2d7
sign peer requests with request-bound proofs
the-code-learner Aug 18, 2026
695e86a
enforce chain-aware replay-safe peer admission
the-code-learner Aug 18, 2026
a0b1530
add versioned state commitment domain
the-code-learner Aug 18, 2026
62341f0
add deterministic chain-bound state commitments
the-code-learner Aug 18, 2026
464a054
validate snapshots against quorum-signed committed state
the-code-learner Aug 18, 2026
a20484f
add versioned block signing domain
the-code-learner Aug 18, 2026
06e3776
commit chain and state root into block identity
the-code-learner Aug 18, 2026
68f639a
produce certified blocks with chain-bound state roots
the-code-learner Aug 18, 2026
cf309b4
sign committed snapshots with validator proofs
the-code-learner Aug 18, 2026
102d4ec
leave internal request authentication to endpoint handlers
the-code-learner Aug 18, 2026
82e5e5b
add temporary protocol backend integration verifier
the-code-learner Aug 18, 2026
fa4e469
fix protocol refresh snapshot selector
the-code-learner Aug 18, 2026
6c2d91d
run corrected protocol backend integration
the-code-learner Aug 18, 2026
7c90166
add protocol backend integration script
the-code-learner Aug 18, 2026
7327e34
run protocol integration from repository script
the-code-learner Aug 18, 2026
4461de1
add chain-bound transaction and node status types
the-code-learner Aug 18, 2026
f8bc1e8
read chain id from node status
the-code-learner Aug 18, 2026
797b3f4
add wallet protocol integration script
the-code-learner Aug 18, 2026
d2d36cd
repurpose temporary workflow for wallet protocol verification
the-code-learner Aug 18, 2026
71fe260
bind wallet signatures to node chain identity
github-actions[bot] Aug 18, 2026
ee5da88
adapt protocol test fixtures to clean-break primitives
the-code-learner Aug 18, 2026
4890e8c
apply clean-break fixture updates before backend verification
the-code-learner Aug 18, 2026
5e7e541
capture concise protocol backend test diagnostics
the-code-learner Aug 18, 2026
6d2a87f
capture protocol backend test failures
github-actions[bot] Aug 18, 2026
12a74af
test chain-bound canonical transaction signatures
the-code-learner Aug 18, 2026
662148a
test clean-break consensus signing domains
the-code-learner Aug 18, 2026
df651e4
capture protocol backend test failures
github-actions[bot] Aug 18, 2026
ff6fde3
add proposal state-root validation integration
the-code-learner Aug 18, 2026
8e597a5
verify proposal state roots and clean-break fixtures
the-code-learner Aug 18, 2026
8edabf3
capture protocol backend test failures
github-actions[bot] Aug 18, 2026
dad7b59
adapt clean-break protocol fixtures and adversarial tests
the-code-learner Aug 18, 2026
049477f
close internal endpoint and unsafe snapshot bypasses
the-code-learner Aug 18, 2026
c85af08
verify clean-break protocol fixtures and trust boundaries
the-code-learner Aug 18, 2026
8102168
scope protocol fixture peer configuration patch
the-code-learner Aug 18, 2026
b8dec5e
scope fixture patch before protocol integration
the-code-learner Aug 18, 2026
427e98a
capture protocol backend test failures
github-actions[bot] Aug 18, 2026
f61f90c
finish clean-break fixture migration
the-code-learner Aug 18, 2026
ec962e9
apply final clean-break fixture fixes before verification
the-code-learner Aug 18, 2026
ad7f1ac
capture protocol backend test failures
github-actions[bot] Aug 18, 2026
77cf55f
deduplicate validator fixture and align error assertions
the-code-learner Aug 18, 2026
63d13dd
run final protocol fixture assertions
the-code-learner Aug 18, 2026
76c4a08
capture protocol backend test failures
github-actions[bot] Aug 18, 2026
3020823
align API consensus fixtures with verified state roots
the-code-learner Aug 18, 2026
d8dae6f
align auth statuses and final protocol assertions
the-code-learner Aug 18, 2026
6876cf2
verify state-root API fixtures and auth policy
the-code-learner Aug 18, 2026
0b3d394
capture protocol backend test failures
github-actions[bot] Aug 18, 2026
1209fed
finish authenticated recovery and consensus test invariants
the-code-learner Aug 18, 2026
594b794
verify final authenticated recovery edge cases
the-code-learner Aug 18, 2026
f0bcfb0
integrate chain-bound protocol and snapshot quorum
github-actions[bot] Aug 18, 2026
6a7dd5c
remove temporary protocol wallet workflow
the-code-learner Aug 18, 2026
059b583
remove unused temporary protocol workflow
the-code-learner Aug 18, 2026
0672610
remove temporary protocol backend workflow
the-code-learner Aug 18, 2026
3407536
remove temporary protocol integration script
the-code-learner Aug 18, 2026
d486e2d
remove temporary protocol integration script
the-code-learner Aug 18, 2026
cd13060
remove temporary protocol integration script
the-code-learner Aug 18, 2026
7b11ae7
remove temporary protocol integration script
the-code-learner Aug 18, 2026
9f2a6e0
remove temporary protocol integration script
the-code-learner Aug 18, 2026
111763a
remove temporary protocol integration script
the-code-learner Aug 18, 2026
c1d8fde
remove temporary protocol integration script
the-code-learner Aug 18, 2026
ff56153
remove temporary protocol integration script
the-code-learner Aug 18, 2026
a430e5c
remove temporary protocol integration script
the-code-learner Aug 18, 2026
817a0ab
remove temporary protocol integration script
the-code-learner Aug 18, 2026
24d7708
remove temporary protocol integration script
the-code-learner Aug 18, 2026
3f596a3
remove temporary protocol wallet integration script
the-code-learner Aug 18, 2026
9a5aa76
remove protocol test diagnostics
the-code-learner Aug 18, 2026
62e5e3b
document clean-break protocol security invariants
the-code-learner Aug 18, 2026
d064860
add temporary protocol docs migration
the-code-learner Aug 18, 2026
6c81be1
temporarily migrate devnet protocol documentation
the-code-learner Aug 18, 2026
366aab4
restore permanent CI workflow
the-code-learner Aug 18, 2026
8af63b4
remove temporary protocol documentation workflow
the-code-learner Aug 18, 2026
56cbb06
add post-audit state identity hardening patch
the-code-learner Aug 18, 2026
55aa889
test persisted chain identity and funding-id snapshot commitments
the-code-learner Aug 18, 2026
3e6a93d
test bounded internal peer request body hashing
the-code-learner Aug 18, 2026
34dadfa
verify post-audit state identity hardening
the-code-learner Aug 18, 2026
06c1266
trigger post-audit protocol verification
the-code-learner Aug 18, 2026
fa8390b
bind persisted state and funding ids to chain commitments
github-actions[bot] Aug 18, 2026
540e4c5
remove post-audit verification workflow
the-code-learner Aug 18, 2026
292b138
remove post-audit patch script
the-code-learner Aug 18, 2026
ef714a3
remove post-audit verification trigger
the-code-learner Aug 18, 2026
893d11c
document persisted chain identity and funding commitments
the-code-learner Aug 18, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 14 additions & 2 deletions apps/wallet/src/App.vue
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
<script setup lang="ts">
import { computed, onMounted, ref } from 'vue'
import { broadcastTransaction, fetchAccount, fundAccount, pingNode } from './lib/network'
import { broadcastTransaction, fetchAccount, fetchNodeStatus, fundAccount, pingNode } from './lib/network'
import {
clearAccount,
createAccount,
Expand Down Expand Up @@ -30,6 +30,7 @@ const legacyWalletPresent = ref(false)
const signedEnvelope = ref<SignedTransactionEnvelope | null>(null)
const networkResponse = ref<BroadcastResponse | null>(null)
const networkHealthy = ref<boolean | null>(null)
const chainId = ref('')
const statusMessage = ref('Create, unlock, or import an account to begin.')
const faucetAmount = ref(100)
const isBusy = ref(false)
Expand Down Expand Up @@ -120,8 +121,15 @@ async function refreshNodeState(updateStatus = true) {
async function refreshHealth() {
try {
networkHealthy.value = await pingNode(apiBase.value)
if (!networkHealthy.value) {
chainId.value = ''
return
}
const status = await fetchNodeStatus(apiBase.value)
chainId.value = status.chainId
} catch {
networkHealthy.value = false
chainId.value = ''
}
}

Expand Down Expand Up @@ -265,7 +273,10 @@ async function handleSignTransaction() {
isBusy.value = true

try {
const envelope = await signTransaction(account.value, form.value)
if (!chainId.value) {
throw new Error('Node chain identity is unavailable; refresh the node connection before signing')
}
const envelope = await signTransaction(account.value, form.value, chainId.value)
signedEnvelope.value = envelope
statusMessage.value = 'Transaction signed locally using the unlocked device key.'
} catch (error) {
Expand Down Expand Up @@ -311,6 +322,7 @@ async function handleBroadcast() {
Node health:
{{ networkHealthy === null ? 'checking' : networkHealthy ? 'online' : 'offline' }}
</span>
<span class="pill">Chain: {{ chainId || 'unavailable' }}</span>
<span class="pill">{{ balancePill }}</span>
</div>
</section>
Expand Down
23 changes: 9 additions & 14 deletions apps/wallet/src/lib/network.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import type {
ApiError,
BroadcastResponse,
FaucetResponse,
NodeStatusResponse,
SignedTransactionEnvelope
} from '../types'

Expand All @@ -12,6 +13,11 @@ export async function pingNode(apiBase: string): Promise<boolean> {
return response.ok
}

export async function fetchNodeStatus(apiBase: string): Promise<NodeStatusResponse> {
const response = await fetch(url(apiBase, '/v1/status'))
return readJSON<NodeStatusResponse>(response)
}

export async function fetchAccount(apiBase: string, address: string): Promise<AccountView> {
const response = await fetch(url(apiBase, `/v1/accounts/${encodeURIComponent(address)}`))
const payload = await readJSON<AccountResponse>(response)
Expand All @@ -21,40 +27,29 @@ export async function fetchAccount(apiBase: string, address: string): Promise<Ac
export async function fundAccount(apiBase: string, address: string, amount: number): Promise<AccountView> {
const response = await fetch(url(apiBase, '/v1/dev/faucet'), {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ address, amount })
})

const payload = await readJSON<FaucetResponse>(response)
return payload.account
}

export async function broadcastTransaction(
apiBase: string,
envelope: SignedTransactionEnvelope
): Promise<BroadcastResponse> {
export async function broadcastTransaction(apiBase: string, envelope: SignedTransactionEnvelope): Promise<BroadcastResponse> {
const response = await fetch(url(apiBase, '/v1/transactions'), {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(envelope)
})

return readJSON<BroadcastResponse>(response)
}

async function readJSON<T>(response: Response): Promise<T> {
const raw = await response.text()
const payload = raw ? (JSON.parse(raw) as T | ApiError) : null

if (!response.ok) {
const message = payload && typeof payload === 'object' && 'error' in payload ? payload.error : undefined
throw new Error(message || `Request failed with status ${response.status}`)
}

return payload as T
}

Expand Down
54 changes: 49 additions & 5 deletions apps/wallet/src/lib/wallet.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,9 @@ const MIN_PASSPHRASE_LENGTH = 10
const KDF_ITERATIONS = 310_000
const MIN_KDF_ITERATIONS = 100_000
const MAX_KDF_ITERATIONS = 1_000_000
const TRANSACTION_DOMAIN = 'zephyr/transaction/v1' as const
const P256_ORDER = BigInt('0xffffffff00000000ffffffffffffffffbce6faada7179e84f3b9cac2fc632551')
const P256_HALF_ORDER = P256_ORDER >> 1n

export async function createAccount(): Promise<StoredAccount> {
const keyPair = await crypto.subtle.generateKey(
Expand Down Expand Up @@ -107,9 +110,14 @@ export async function importAccount(raw: string, passphrase: string): Promise<St

export async function signTransaction(
account: StoredAccount,
draft: TransactionDraft
draft: TransactionDraft,
chainId: string
): Promise<SignedTransactionEnvelope> {
await assertAccountIntegrity(account)
chainId = chainId.trim()
if (!/^[A-Za-z0-9._-]{1,64}$/.test(chainId)) {
throw new Error('Connect to a node with a valid Zephyr chain ID before signing')
}

const privateKey = await crypto.subtle.importKey(
'jwk',
Expand All @@ -123,11 +131,13 @@ export async function signTransaction(
)

const payload = canonicalize({
from: draft.from,
to: draft.to,
amount: draft.amount,
chainId,
domain: TRANSACTION_DOMAIN,
from: draft.from,
memo: draft.memo,
nonce: draft.nonce,
memo: draft.memo
to: draft.to
})

const signature = await crypto.subtle.sign(
Expand All @@ -143,7 +153,9 @@ export async function signTransaction(
...draft,
payload,
publicKey: account.publicKeySpki,
signature: bytesToBase64(new Uint8Array(signature))
chainId,
domain: TRANSACTION_DOMAIN,
signature: bytesToBase64(normalizeP256Signature(new Uint8Array(signature)))
}
}

Expand Down Expand Up @@ -427,6 +439,38 @@ function sortObject(value: unknown): unknown {
return value
}

function normalizeP256Signature(signature: Uint8Array): Uint8Array {
if (signature.length !== 64) {
throw new Error('Browser returned an invalid P-256 signature')
}
const r = bytesToBigInt(signature.slice(0, 32))
let s = bytesToBigInt(signature.slice(32))
if (s > P256_HALF_ORDER) {
s = P256_ORDER - s
}
const normalized = new Uint8Array(64)
normalized.set(bigIntTo32Bytes(r), 0)
normalized.set(bigIntTo32Bytes(s), 32)
return normalized
}

function bytesToBigInt(bytes: Uint8Array): bigint {
const hex = bytesToHex(bytes) || '0'
return BigInt(`0x${hex}`)
}

function bigIntTo32Bytes(value: bigint): Uint8Array {
const hex = value.toString(16).padStart(64, '0')
if (hex.length > 64) {
throw new Error('P-256 signature integer is out of range')
}
const bytes = new Uint8Array(32)
for (let index = 0; index < 32; index += 1) {
bytes[index] = Number.parseInt(hex.slice(index * 2, index * 2 + 2), 16)
}
return bytes
}

function bytesToHex(bytes: Uint8Array): string {
return Array.from(bytes, (byte) => byte.toString(16).padStart(2, '0')).join('')
}
Expand Down
8 changes: 8 additions & 0 deletions apps/wallet/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,11 +32,18 @@ export type TransactionDraft = {
}

export type SignedTransactionEnvelope = TransactionDraft & {
chainId: string
domain: 'zephyr/transaction/v1'
payload: string
publicKey: string
signature: string
}

export type NodeStatusResponse = {
chainId: string
nodeId: string
}

export type BroadcastResponse = {
id: string
accepted: boolean
Expand All @@ -51,6 +58,7 @@ export type AccountView = {
nonce: number
nextNonce: number
pendingTransactions: number
nonceExhausted?: boolean
}

export type AccountResponse = {
Expand Down
6 changes: 5 additions & 1 deletion cmd/node/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,9 @@ func main() {

config := api.DefaultConfig()
enableDevEndpoints := false
if chainID := os.Getenv("ZEPHYR_CHAIN_ID"); chainID != "" {
config.ChainID = chainID
}
if nodeID := os.Getenv("ZEPHYR_NODE_ID"); nodeID != "" {
config.NodeID = nodeID
}
Expand Down Expand Up @@ -145,8 +148,9 @@ func main() {
defer server.Close()

log.Printf(
"zephyr node %s listening on %s (validator: %s, data dir: %s, block interval: %s, consensus automation: %t, consensus interval: %s, round timeout: %s, peer sync: %t, structured logs: %t, peer identity required: %t, peer bindings: %d, proposer schedule enforced: %t, consensus certificates required: %t, dev endpoints: %t, peers: %d)",
"zephyr node %s on chain %s listening on %s (validator: %s, data dir: %s, block interval: %s, consensus automation: %t, consensus interval: %s, round timeout: %s, peer sync: %t, structured logs: %t, peer identity required: %t, peer bindings: %d, proposer schedule enforced: %t, consensus certificates required: %t, dev endpoints: %t, peers: %d)",
config.NodeID,
config.ChainID,
addr,
config.ValidatorAddress,
config.DataDir,
Expand Down
74 changes: 74 additions & 0 deletions docs/protocol-security.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
# Zephyr Protocol Security Model

This document defines the protocol-security invariants implemented for issues #2, #3, #4, and #5. The current protocol is a clean break: there is no active devnet or deployed state that requires legacy message compatibility.

## Network identity

The default local-development network identifier is `zephyr-devnet-1`. Public testnets and production networks must use distinct, stable chain IDs through `ZEPHYR_CHAIN_ID`.

`chainId` is mandatory in security-critical protocol messages. A verifier never fills in a missing chain ID on behalf of a received message. Nodes reject transactions, consensus messages, blocks, peer request proofs, and snapshots that target a different chain.

The persisted node state also records its chain ID. A newly initialized data directory is bound to the configured chain immediately, and reopening that directory with a different chain ID fails closed. Persisted blocks are checked against the same chain identity before state is loaded, preventing accidental hybrid histories when operators reuse a data directory.

## Versioned domains

Protocol objects are separated by explicit versioned domains:

- `zephyr/transaction/v1`
- `zephyr/block/v1`
- `zephyr/consensus/proposal/v1`
- `zephyr/consensus/vote/v1`
- `zephyr/transport/identity/v1`
- `zephyr/transport/request/v1`
- `zephyr/state/v1`
- `zephyr/snapshot/v1`

The relevant domain and chain ID are part of each canonical signed or hashed representation. A signature or commitment from one message type or network is therefore not valid in another domain or network.

## Canonical P-256 signatures and transaction identity

Zephyr uses raw 64-byte P-256 ECDSA signatures encoded as base64 (`r || s`). Every Zephyr signer normalizes `s` to low-S and verification rejects high-S signatures.

A transaction ID hashes the canonical transaction identity, including chain ID, signing domain, sender, recipient, amount, nonce, memo, and public key. Signature bytes are deliberately excluded, so equivalent ECDSA encodings cannot produce distinct transaction IDs.

The browser wallet reads the chain ID from the connected node before signing and refuses to sign while the node network identity is unavailable.

## Chain-bound blocks and state roots

Every block contains `chainId` and `stateRoot`. Both values are part of the canonical block hash.

The state root is a deterministic `zephyr/state/v1` commitment over consensus-critical committed account state, the active validator snapshot, and the sorted set of already-applied funding request IDs. Including funding IDs preserves faucet/funding idempotency across quorum snapshot recovery: a previously applied request cannot become valid again merely because a node restored state from peers. Volatile node-local telemetry, pending diagnostics, and local timing metadata are excluded.

Before recording or voting for a proposal, a validator deterministically executes the proposal transactions against its committed state and recomputes the expected state root. A proposal whose signed root does not match local execution is rejected. This makes the quorum certificate commit to both the transaction body and its resulting state.

## Request-bound peer authentication

The signed identity exposed in node status is a discovery/admission proof only. It does not authorize an internal request.

Every authenticated peer request binds the validator identity to:

- chain ID and `zephyr/transport/request/v1`
- HTTP method
- canonical request path and query
- SHA-256 request-body hash
- node and validator identity
- a cryptographically random nonce
- a bounded timestamp

The receiver verifies the complete proof before accepting internal state-changing or snapshot operations. Used nonces are stored in an owner-only replay database with bounded expiry. Replay of the same proof, reuse on a different endpoint/body, and reuse after node restart are rejected.

Request-body hashing is bounded by the same 1 MiB process limit even when the server is embedded through its internal handler, so authentication cannot be used to bypass the public HTTP memory limit.

All `/v1/internal/*` endpoints require a signed peer request proof even when the server is embedded through the internal handler rather than the process-facing public wrapper.

## Snapshot trust and quorum

Peer authentication proves who sent a snapshot; it does not prove that the snapshot is correct. Snapshot restore therefore requires independent state validation and validator quorum.

A snapshot proof is bound to the local chain ID, `zephyr/snapshot/v1`, committed height, latest block hash, state commitment, and validator-set version. The node validates block continuity and hashes, chain-bound transaction IDs and signatures, committed account state, applied funding IDs, validator voting-power arithmetic, and the latest committed state root before considering a proof.

The node uses its locally trusted validator set as the recovery trust anchor. Matching snapshot proofs from distinct validators are accumulated by voting power and restore is allowed only after the normal 2/3+ quorum threshold is reached. A single authenticated validator below quorum cannot replace local state.

A node with no trusted validator set cannot bootstrap itself from an arbitrary peer snapshot. Initial bootstrap must instead establish trust from genesis/checkpoint configuration or validated block synchronization before quorum snapshot recovery becomes available.

After a quorum-approved restore, the node restores only verified committed state, including committed funding-idempotency metadata. Remote mempool contents, pending round state, pending proposals/votes, and remote diagnostics are not blindly adopted; local volatile state is restarted from a clean recovery boundary.
8 changes: 8 additions & 0 deletions internal/api/consensus_api.go
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,10 @@ func (s *Server) handleConsensusProposal(w http.ResponseWriter, r *http.Request)
if request.ProposedAt.IsZero() {
request.ProposedAt = time.Now().UTC()
}
if err := request.ValidateForChain(s.config.ChainID); err != nil {
writeJSON(w, statusForError(err), map[string]string{"error": err.Error()})
return
}

sourceNode := requestSourceNode(r)
var err error
Expand Down Expand Up @@ -102,6 +106,10 @@ func (s *Server) handleConsensusVote(w http.ResponseWriter, r *http.Request) {
if request.VotedAt.IsZero() {
request.VotedAt = time.Now().UTC()
}
if err := request.ValidateForChain(s.config.ChainID); err != nil {
writeJSON(w, statusForError(err), map[string]string{"error": err.Error()})
return
}

sourceNode := requestSourceNode(r)
var (
Expand Down
3 changes: 2 additions & 1 deletion internal/api/consensus_automation.go
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,7 @@ func (s *Server) maybeAutomateProposal(now time.Time) (bool, error) {
if previousProposal, exists := s.ledger.LatestProposalForHeight(consensusView.NextHeight); exists && previousProposal.Round < consensusView.CurrentRound {
proposal.BlockHash = previousProposal.BlockHash
proposal.PreviousHash = previousProposal.PreviousHash
proposal.StateRoot = previousProposal.StateRoot
proposal.ProducedAt = previousProposal.ProducedAt
proposal.TransactionIDs = append([]string(nil), previousProposal.TransactionIDs...)
proposal.Transactions = append([]tx.Envelope(nil), previousProposal.Transactions...)
Expand All @@ -142,6 +143,7 @@ func (s *Server) maybeAutomateProposal(now time.Time) (bool, error) {
}
proposal.BlockHash = block.Hash
proposal.PreviousHash = block.PreviousHash
proposal.StateRoot = block.StateRoot
proposal.ProducedAt = block.ProducedAt
proposal.TransactionIDs = append([]string(nil), block.TransactionIDs...)
proposal.Transactions = append([]tx.Envelope(nil), block.Transactions...)
Expand Down Expand Up @@ -294,4 +296,3 @@ func ignoreConsensusAutomationError(err error) bool {
return false
}
}

Loading