@openzeppelin/miden-multisig-client 0.18.0-rc.1 → 0.18.0-rc.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +26 -14
- package/dist/index.d.ts +2 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -2
- package/dist/index.js.map +1 -1
- package/dist/multisig/authArgErrors.d.ts +15 -1
- package/dist/multisig/authArgErrors.d.ts.map +1 -1
- package/dist/multisig/authArgErrors.js +20 -0
- package/dist/multisig/authArgErrors.js.map +1 -1
- package/dist/multisig.d.ts +36 -16
- package/dist/multisig.d.ts.map +1 -1
- package/dist/multisig.js +109 -58
- package/dist/multisig.js.map +1 -1
- package/dist/prover/workflow.d.ts +7 -3
- package/dist/prover/workflow.d.ts.map +1 -1
- package/dist/prover/workflow.js +7 -5
- package/dist/prover/workflow.js.map +1 -1
- package/dist/transaction/summary.d.ts +92 -14
- package/dist/transaction/summary.d.ts.map +1 -1
- package/dist/transaction/summary.js +93 -3
- package/dist/transaction/summary.js.map +1 -1
- package/dist/transaction.d.ts +1 -1
- package/dist/transaction.d.ts.map +1 -1
- package/dist/transaction.js +1 -1
- package/dist/transaction.js.map +1 -1
- package/dist/types/proposal.d.ts +9 -8
- package/dist/types/proposal.d.ts.map +1 -1
- package/dist/types/proposal.js.map +1 -1
- package/package.json +4 -4
- package/src/index.ts +3 -0
- package/src/multisig/authArgErrors.ts +28 -1
- package/src/multisig.ts +127 -65
- package/src/prover/workflow.ts +7 -10
- package/src/transaction/summary.ts +161 -16
- package/src/transaction.ts +6 -0
- package/src/types/proposal.ts +9 -8
package/dist/multisig.js
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
import { GuardianHttpClient } from '@openzeppelin/guardian-client';
|
|
8
8
|
import { ProposalSaltMalformedError } from './multisig/authArgErrors.js';
|
|
9
9
|
import { Account, AccountId, AdviceMap, FeltArray, Note, NoteExportFormat, NoteFile, Signature, TransactionRequest, TransactionSummary, Word, } from '@miden-sdk/miden-sdk';
|
|
10
|
-
import { chainAnchorFromBase64, chainAnchorToBase64, executeForSummary,
|
|
10
|
+
import { chainAnchorFromBase64, chainAnchorToBase64, executeForSummary, executeForSummaryAtTip, isStaleChainError, prepareTipExecution, syncToBoundBlock, summaryApprovalExpirationBlockNum, summarySalt, buildUpdateSignersTransactionRequest, buildUpdateProcedureThresholdTransactionRequest, buildUpdateGuardianTransactionRequest, buildConsumeNotesTransactionRequest, buildP2idNoteFromMetadata, buildP2idTransactionRequest, parseP2idNoteType, p2idNoteTypeToMetadata, } from './transaction.js';
|
|
11
11
|
import { buildConsumeNotesTransactionRequestFromNotes } from './transaction/consumeNotes.js';
|
|
12
12
|
import { validateMultisigConfig } from './account/builder.js';
|
|
13
13
|
import { ensureNotesAuthenticated } from './transaction/noteAuthentication.js';
|
|
@@ -539,11 +539,12 @@ export class Multisig {
|
|
|
539
539
|
* Every synced proposal's metadata is checked against its signed summary
|
|
540
540
|
* and the outcome is recorded in {@link Proposal.verification}. One that
|
|
541
541
|
* fails is still cached and returned, so a single stale or corrupt
|
|
542
|
-
* proposal cannot hide the others (issue #462
|
|
543
|
-
* proposal
|
|
544
|
-
* with `retryable: true` means a transient node error
|
|
545
|
-
*
|
|
546
|
-
*
|
|
542
|
+
* proposal cannot hide the others (issue #462). Verification re-executes
|
|
543
|
+
* each proposal at the chain tip, so the Miden client is synced once first.
|
|
544
|
+
* `failed` with `retryable: true` means a transient node error or chain
|
|
545
|
+
* state this client had not caught up with, worth syncing again;
|
|
546
|
+
* `retryable: false` means the proposal cannot be reproduced and must be
|
|
547
|
+
* re-proposed. `signProposal` and `executeProposal` re-verify and
|
|
547
548
|
* refuse a failed proposal. A failed proposal still counts as reported, so
|
|
548
549
|
* it is pruned like any other once GUARDIAN stops listing it.
|
|
549
550
|
*
|
|
@@ -581,6 +582,12 @@ export class Multisig {
|
|
|
581
582
|
const deltas = await this.guardian.getDeltaProposals(this._accountId);
|
|
582
583
|
const factory = this.proposalFactory();
|
|
583
584
|
const reported = new Map();
|
|
585
|
+
if (deltas.length > 0) {
|
|
586
|
+
// Verification re-executes at the store's sync height; bring it to the
|
|
587
|
+
// tip once for the whole listing. Best effort: a node outage then shows
|
|
588
|
+
// up on each proposal as a retryable failure instead of failing the sync.
|
|
589
|
+
await this.syncChain().catch((error) => console.warn('Could not sync the Miden client before verifying proposals', error));
|
|
590
|
+
}
|
|
584
591
|
for (const delta of deltas) {
|
|
585
592
|
const proposalId = normalizeHexWord(computeCommitmentFromTxSummary(delta.deltaPayload.txSummary.data));
|
|
586
593
|
const existingProposal = this.proposals.get(proposalId);
|
|
@@ -1464,6 +1471,8 @@ export class Multisig {
|
|
|
1464
1471
|
}
|
|
1465
1472
|
async signProposal(proposalId) {
|
|
1466
1473
|
const normalizedProposalId = normalizeHexWord(proposalId);
|
|
1474
|
+
// Verification re-executes the proposal at the store's sync height.
|
|
1475
|
+
await this.syncChain();
|
|
1467
1476
|
const existingProposal = await this.getProposalForSigning(proposalId, normalizedProposalId);
|
|
1468
1477
|
if (!existingProposal) {
|
|
1469
1478
|
throw new Error(`Proposal not found: ${proposalId}`);
|
|
@@ -1492,9 +1501,14 @@ export class Multisig {
|
|
|
1492
1501
|
await this.syncProposals();
|
|
1493
1502
|
return this.proposals.get(proposalId) ?? this.proposals.get(normalizedProposalId);
|
|
1494
1503
|
}
|
|
1504
|
+
/**
|
|
1505
|
+
* Builds the final, fully signed request for a ready proposal, for a caller
|
|
1506
|
+
* that proves and submits it with its own pipeline. The request declares the
|
|
1507
|
+
* block its summary binds, so execute it at the chain tip, without an
|
|
1508
|
+
* anchor, on a client synced to at least that block.
|
|
1509
|
+
*/
|
|
1495
1510
|
async createTransactionProposalRequest(proposalId) {
|
|
1496
|
-
const { finalRequest
|
|
1497
|
-
anchor.free();
|
|
1511
|
+
const { finalRequest } = await this.prepareProposalExecution(proposalId);
|
|
1498
1512
|
return finalRequest;
|
|
1499
1513
|
}
|
|
1500
1514
|
/**
|
|
@@ -1503,22 +1517,16 @@ export class Multisig {
|
|
|
1503
1517
|
* @param proposalId - The proposal commitment/ID
|
|
1504
1518
|
*/
|
|
1505
1519
|
async executeProposal(proposalId) {
|
|
1506
|
-
const { metadata, finalRequest, proposal
|
|
1507
|
-
|
|
1508
|
-
|
|
1509
|
-
|
|
1510
|
-
|
|
1511
|
-
|
|
1512
|
-
|
|
1513
|
-
|
|
1514
|
-
|
|
1515
|
-
|
|
1516
|
-
// against the summary's block commitment during binding verification.
|
|
1517
|
-
await this.proverWorkflow.submitAt(AccountId.fromHex(this._accountId), finalRequest, anchor);
|
|
1518
|
-
}
|
|
1519
|
-
finally {
|
|
1520
|
-
anchor.free();
|
|
1521
|
-
}
|
|
1520
|
+
const { metadata, finalRequest, proposal } = await this.prepareProposalExecution(proposalId);
|
|
1521
|
+
if (metadata.proposalType === 'switch_guardian') {
|
|
1522
|
+
// #417: import notes embedded in pending proposals from the old
|
|
1523
|
+
// GUARDIAN. Must run before the switch executes and repoints;
|
|
1524
|
+
// best-effort and bounded — see preservePreSwitchProposalNotes.
|
|
1525
|
+
await this.preservePreSwitchProposalNotes();
|
|
1526
|
+
}
|
|
1527
|
+
// Execute at the chain tip. The request declares the block the signed
|
|
1528
|
+
// summary binds, so the summary the cosigners signed reproduces there.
|
|
1529
|
+
await this.submitAtTip(finalRequest);
|
|
1522
1530
|
if (metadata.proposalType === 'switch_guardian') {
|
|
1523
1531
|
if (!metadata.newGuardianEndpoint || !metadata.newGuardianPubkey) {
|
|
1524
1532
|
throw new Error('Switch GUARDIAN proposal metadata is incomplete after execution');
|
|
@@ -1564,11 +1572,12 @@ export class Multisig {
|
|
|
1564
1572
|
* Submit an integration-built transaction (advice already injected). Mirrors
|
|
1565
1573
|
* the Rust `submit_transaction`; used by the custom proposal producer flow
|
|
1566
1574
|
* after `prepareCustomExecution` rebuilds its request with the returned advice.
|
|
1567
|
-
* The transaction is executed at the
|
|
1568
|
-
*
|
|
1575
|
+
* The transaction is executed at the chain tip, so the request has to declare
|
|
1576
|
+
* the block its auth args bind (`feeAwareTransactionRequestBuilder` does).
|
|
1569
1577
|
*/
|
|
1570
1578
|
async submitTransaction(proposalId, request) {
|
|
1571
1579
|
const normalizedProposalId = normalizeHexWord(proposalId);
|
|
1580
|
+
await this.syncChain();
|
|
1572
1581
|
const delta = await this.guardian.getDeltaProposal(this._accountId, normalizedProposalId);
|
|
1573
1582
|
const existing = this.getLocalProposal(proposalId);
|
|
1574
1583
|
const proposal = this.proposalFactory().fromDelta(delta, normalizedProposalId, existing?.metadata, existing?.signatures ?? []);
|
|
@@ -1580,11 +1589,34 @@ export class Multisig {
|
|
|
1580
1589
|
if (anchorCommitment !== summaryBlockCommitment) {
|
|
1581
1590
|
throw new Error(`Proposal ${proposalId} chain anchor does not match the block commitment bound into its tx_summary`);
|
|
1582
1591
|
}
|
|
1583
|
-
await this.proverWorkflow.submitAt(AccountId.fromHex(this._accountId), request, anchor);
|
|
1584
1592
|
}
|
|
1585
1593
|
finally {
|
|
1586
1594
|
anchor.free();
|
|
1587
1595
|
}
|
|
1596
|
+
await this.submitAtTip(request);
|
|
1597
|
+
}
|
|
1598
|
+
/**
|
|
1599
|
+
* Executes `request` at the chain tip, then proves, submits and applies it,
|
|
1600
|
+
* syncing first when this client is still below the block the request binds.
|
|
1601
|
+
*/
|
|
1602
|
+
async submitAtTip(request) {
|
|
1603
|
+
const webClient = await this.getRawClient();
|
|
1604
|
+
await prepareTipExecution(webClient, request, () => retryRpcRead(() => webClient.syncState(), this.rpcConfig));
|
|
1605
|
+
await this.proverWorkflow.submit(AccountId.fromHex(this._accountId), request);
|
|
1606
|
+
}
|
|
1607
|
+
/**
|
|
1608
|
+
* Brings the Miden client to the chain tip before a proposal is re-executed:
|
|
1609
|
+
* an execution loads foreign accounts, the fee faucet among them, at the
|
|
1610
|
+
* store's sync height, which a node prunes about 50 blocks later.
|
|
1611
|
+
*/
|
|
1612
|
+
async syncChain() {
|
|
1613
|
+
const webClient = await this.getRawClient();
|
|
1614
|
+
await retryRpcRead(() => webClient.syncState(), this.rpcConfig);
|
|
1615
|
+
}
|
|
1616
|
+
/** {@link syncToBoundBlock} with this client's RPC retry policy. */
|
|
1617
|
+
async syncToBoundBlock(boundBlockNum) {
|
|
1618
|
+
const webClient = await this.getRawClient();
|
|
1619
|
+
await syncToBoundBlock(webClient, boundBlockNum, () => retryRpcRead(() => webClient.syncState(), this.rpcConfig));
|
|
1588
1620
|
}
|
|
1589
1621
|
/**
|
|
1590
1622
|
* Create a proposal from a producer-built transaction the SDK does not model.
|
|
@@ -1632,6 +1664,8 @@ export class Multisig {
|
|
|
1632
1664
|
*/
|
|
1633
1665
|
async prepareCustomExecution(proposalId, transactionRequestBytes) {
|
|
1634
1666
|
const normalizedProposalId = normalizeHexWord(proposalId);
|
|
1667
|
+
// The binding probe re-executes the producer's request at the store's sync height.
|
|
1668
|
+
await this.syncChain();
|
|
1635
1669
|
const delta = await this.guardian.getDeltaProposal(this._accountId, normalizedProposalId);
|
|
1636
1670
|
const existing = this.getLocalProposal(proposalId);
|
|
1637
1671
|
const proposal = this.proposalFactory().fromDelta(delta, normalizedProposalId, existing?.metadata, existing?.signatures ?? []);
|
|
@@ -1646,26 +1680,24 @@ export class Multisig {
|
|
|
1646
1680
|
const txSummary = TransactionSummary.deserialize(base64ToUint8Array(delta.deltaPayload.txSummary.data));
|
|
1647
1681
|
const signedCommitmentHex = normalizeHexWord(txSummary.toCommitment().toHex());
|
|
1648
1682
|
const bindingRequest = deserializeTransactionRequest(transactionRequestBytes);
|
|
1649
|
-
//
|
|
1650
|
-
//
|
|
1651
|
-
//
|
|
1652
|
-
//
|
|
1653
|
-
// before executing against it.
|
|
1683
|
+
// The anchor arrives from an untrusted party via GUARDIAN, so its block
|
|
1684
|
+
// commitment is checked against the signed summary: it has to name the
|
|
1685
|
+
// block the summary binds. The probe itself runs at the chain tip, where
|
|
1686
|
+
// the request's declared bound block reproduces the signed summary.
|
|
1654
1687
|
const anchor = this.requireProposalAnchor(proposalId, proposal.metadata);
|
|
1655
|
-
let derivedCommitmentHex;
|
|
1656
1688
|
try {
|
|
1657
1689
|
const anchorCommitment = normalizeHexWord(anchor.commitment().toHex());
|
|
1658
1690
|
const summaryBlockCommitment = normalizeHexWord(txSummary.blockCommitment().toHex());
|
|
1659
1691
|
if (anchorCommitment !== summaryBlockCommitment) {
|
|
1660
1692
|
throw new Error(`Custom proposal ${proposalId} chain anchor does not match the block commitment bound into its tx_summary`);
|
|
1661
1693
|
}
|
|
1662
|
-
const webClient = await this.getRawClient();
|
|
1663
|
-
const derived = await executeForSummaryAt(webClient, this._accountId, bindingRequest, anchor);
|
|
1664
|
-
derivedCommitmentHex = normalizeHexWord(derived.toCommitment().toHex());
|
|
1665
1694
|
}
|
|
1666
1695
|
finally {
|
|
1667
1696
|
anchor.free();
|
|
1668
1697
|
}
|
|
1698
|
+
const webClient = await this.getRawClient();
|
|
1699
|
+
const derived = await executeForSummaryAtTip(webClient, this._accountId, bindingRequest);
|
|
1700
|
+
const derivedCommitmentHex = normalizeHexWord(derived.toCommitment().toHex());
|
|
1669
1701
|
if (derivedCommitmentHex !== signedCommitmentHex) {
|
|
1670
1702
|
throw new Error(`Custom proposal binding mismatch: expected ${signedCommitmentHex}, got ${derivedCommitmentHex}`);
|
|
1671
1703
|
}
|
|
@@ -1753,8 +1785,8 @@ export class Multisig {
|
|
|
1753
1785
|
return this.proposals.get(proposalId) ?? this.proposals.get(normalizedProposalId);
|
|
1754
1786
|
}
|
|
1755
1787
|
/**
|
|
1756
|
-
* The returned
|
|
1757
|
-
*
|
|
1788
|
+
* The returned request declares the block the signed summary binds, and
|
|
1789
|
+
* executes at the chain tip.
|
|
1758
1790
|
*/
|
|
1759
1791
|
async prepareProposalExecution(proposalId) {
|
|
1760
1792
|
const proposal = this.getLocalProposal(proposalId);
|
|
@@ -1762,6 +1794,8 @@ export class Multisig {
|
|
|
1762
1794
|
throw new Error(`Proposal not found: ${proposalId}`);
|
|
1763
1795
|
}
|
|
1764
1796
|
this.proposalFactory().assertAccountId(proposal.accountId);
|
|
1797
|
+
// Verification and execution run at the store's sync height.
|
|
1798
|
+
await this.syncChain();
|
|
1765
1799
|
await this.verifyProposalMetadataBinding(proposal);
|
|
1766
1800
|
const metadata = proposal.metadata;
|
|
1767
1801
|
// Reject custom proposals before any advice assembly or GUARDIAN ack push:
|
|
@@ -1873,14 +1907,18 @@ export class Multisig {
|
|
|
1873
1907
|
await this.verifyGuardianEndpointCommitment(metadata.newGuardianEndpoint, metadata.newGuardianPubkey);
|
|
1874
1908
|
}
|
|
1875
1909
|
const anchor = this.requireProposalAnchor(proposalId, metadata);
|
|
1910
|
+
let binding;
|
|
1876
1911
|
try {
|
|
1877
|
-
|
|
1878
|
-
return { finalRequest, metadata, proposal, anchor };
|
|
1912
|
+
binding = proposalRequestBinding(txSummary, anchor, saltHex);
|
|
1879
1913
|
}
|
|
1880
|
-
|
|
1914
|
+
finally {
|
|
1881
1915
|
anchor.free();
|
|
1882
|
-
throw error;
|
|
1883
1916
|
}
|
|
1917
|
+
// A switch_guardian proposal is verified without a rebuild, so this may be
|
|
1918
|
+
// the first time this client needs the chain at the bound block.
|
|
1919
|
+
await this.syncToBoundBlock(binding.boundBlockNum);
|
|
1920
|
+
const finalRequest = await this.buildTransactionRequestFromMetadata(metadata, binding, adviceMap);
|
|
1921
|
+
return { finalRequest, metadata, proposal };
|
|
1884
1922
|
}
|
|
1885
1923
|
/**
|
|
1886
1924
|
* Export a proposal for offline signing
|
|
@@ -1950,6 +1988,10 @@ export class Multisig {
|
|
|
1950
1988
|
throw new Error('Invalid proposal JSON: missing required fields');
|
|
1951
1989
|
}
|
|
1952
1990
|
const proposal = this.proposalFactory().fromExported(exported);
|
|
1991
|
+
// Verification re-executes every built-in type at the store's sync height.
|
|
1992
|
+
if (proposal.metadata.proposalType !== 'custom') {
|
|
1993
|
+
await this.syncChain();
|
|
1994
|
+
}
|
|
1953
1995
|
await this.verifyProposalMetadataBinding(proposal);
|
|
1954
1996
|
this.proposals.set(proposal.id, proposal);
|
|
1955
1997
|
return proposal;
|
|
@@ -1982,6 +2024,10 @@ export class Multisig {
|
|
|
1982
2024
|
if (alreadySigned) {
|
|
1983
2025
|
throw new Error('You have already signed this proposal');
|
|
1984
2026
|
}
|
|
2027
|
+
// Verification re-executes every built-in type at the store's sync height.
|
|
2028
|
+
if (proposal.metadata?.proposalType !== 'custom') {
|
|
2029
|
+
await this.syncChain();
|
|
2030
|
+
}
|
|
1985
2031
|
const commitmentToSign = await this.verifyProposalMetadataBinding(proposal);
|
|
1986
2032
|
// Sign the commitment
|
|
1987
2033
|
const signature = await buildGuardianSignatureFromSigner(this.signer, commitmentToSign);
|
|
@@ -2033,7 +2079,7 @@ export class Multisig {
|
|
|
2033
2079
|
catch (error) {
|
|
2034
2080
|
proposal.verification = {
|
|
2035
2081
|
status: 'failed',
|
|
2036
|
-
retryable: isTransientRpcError(error),
|
|
2082
|
+
retryable: isTransientRpcError(error) || isStaleChainError(error),
|
|
2037
2083
|
message: error instanceof Error ? error.message : String(error),
|
|
2038
2084
|
};
|
|
2039
2085
|
throw error;
|
|
@@ -2042,10 +2088,11 @@ export class Multisig {
|
|
|
2042
2088
|
async checkProposalMetadataBinding(proposal) {
|
|
2043
2089
|
const txSummaryCommitment = this.ensureProposalCommitmentMatchesSummary(proposal);
|
|
2044
2090
|
const summary = TransactionSummary.deserialize(base64ToUint8Array(proposal.txSummary));
|
|
2045
|
-
// The anchor arrives from an untrusted party via GUARDIAN, so check
|
|
2046
|
-
//
|
|
2047
|
-
//
|
|
2048
|
-
//
|
|
2091
|
+
// The anchor arrives from an untrusted party via GUARDIAN, so check that it
|
|
2092
|
+
// names the block the signed summary binds: the rebuild below binds the
|
|
2093
|
+
// block it names. Nothing executes against it; the rebuild runs at the
|
|
2094
|
+
// chain tip. `ChainAnchor.deserialize` already enforced internal
|
|
2095
|
+
// header/chain consistency.
|
|
2049
2096
|
const anchor = this.requireProposalAnchor(proposal.id, proposal.metadata);
|
|
2050
2097
|
try {
|
|
2051
2098
|
const anchorCommitment = normalizeHexWord(anchor.commitment().toHex());
|
|
@@ -2072,6 +2119,12 @@ export class Multisig {
|
|
|
2072
2119
|
// binding checks for this type.
|
|
2073
2120
|
return txSummaryCommitment;
|
|
2074
2121
|
}
|
|
2122
|
+
// The rebuild reads the chain's fee faucet from the synced protocol
|
|
2123
|
+
// configuration and executes at the tip, so the store has to have synced
|
|
2124
|
+
// to the block the summary binds; a cosigner that has only just loaded
|
|
2125
|
+
// the account has not.
|
|
2126
|
+
await this.syncToBoundBlock(binding.boundBlockNum);
|
|
2127
|
+
const webClient = await this.getRawClient();
|
|
2075
2128
|
// A consume-notes summary commits to *authenticated* consumption (see
|
|
2076
2129
|
// ensureNotesAuthenticated), which miden-client decides from this store
|
|
2077
2130
|
// alone. Put the store in that mode before the rebuild, or a cosigner
|
|
@@ -2081,8 +2134,7 @@ export class Multisig {
|
|
|
2081
2134
|
await this.ensureNotesAuthenticated(decodeEmbeddedConsumeNotes(proposal.metadata));
|
|
2082
2135
|
}
|
|
2083
2136
|
const request = await this.buildTransactionRequestFromMetadata(proposal.metadata, binding);
|
|
2084
|
-
const
|
|
2085
|
-
const reconstructed = await executeForSummaryAt(webClient, this._accountId, request, anchor);
|
|
2137
|
+
const reconstructed = await executeForSummaryAtTip(webClient, this._accountId, request);
|
|
2086
2138
|
const reconstructedCommitment = normalizeHexWord(reconstructed.toCommitment().toHex());
|
|
2087
2139
|
if (reconstructedCommitment !== txSummaryCommitment) {
|
|
2088
2140
|
throw new Error(`Invalid proposal: metadata does not match tx_summary for ${proposal.id}`);
|
|
@@ -2093,12 +2145,6 @@ export class Multisig {
|
|
|
2093
2145
|
anchor.free();
|
|
2094
2146
|
}
|
|
2095
2147
|
}
|
|
2096
|
-
/**
|
|
2097
|
-
* Decodes a proposal's chain anchor. Throws when absent: a proposal without
|
|
2098
|
-
* an anchor was created at an unknown reference block, so its signed summary
|
|
2099
|
-
* cannot be reproduced, verified, or executed. The caller owns the returned
|
|
2100
|
-
* anchor and must `free()` it once done.
|
|
2101
|
-
*/
|
|
2102
2148
|
/**
|
|
2103
2149
|
* Reads a proposal's salt. Throws when absent, because there is nothing to fall
|
|
2104
2150
|
* back to.
|
|
@@ -2140,11 +2186,16 @@ export class Multisig {
|
|
|
2140
2186
|
}
|
|
2141
2187
|
return saltHex;
|
|
2142
2188
|
}
|
|
2189
|
+
/**
|
|
2190
|
+
* Decodes a proposal's chain anchor, which names the block its signed summary
|
|
2191
|
+
* binds. Throws when absent: every proposal carries one, so a proposal
|
|
2192
|
+
* without it is malformed and is neither verified nor executed. The caller
|
|
2193
|
+
* owns the returned anchor and must `free()` it once done.
|
|
2194
|
+
*/
|
|
2143
2195
|
requireProposalAnchor(proposalId, metadata) {
|
|
2144
2196
|
if (!metadata.chainAnchor) {
|
|
2145
|
-
throw new Error(`Proposal ${proposalId} has no chain anchor
|
|
2146
|
-
'
|
|
2147
|
-
'at the original reference block');
|
|
2197
|
+
throw new Error(`Proposal ${proposalId} has no chain anchor, which names the block its signed ` +
|
|
2198
|
+
'summary binds; it cannot be verified or executed');
|
|
2148
2199
|
}
|
|
2149
2200
|
return chainAnchorFromBase64(metadata.chainAnchor);
|
|
2150
2201
|
}
|