feat(covenantsigner): wallet-signed EIP-712 approvals + cooperative REDEEM/RENEW#4169
feat(covenantsigner): wallet-signed EIP-712 approvals + cooperative REDEEM/RENEW#4169piotr-roslaniec wants to merge 4 commits into
Conversation
…-signed ETH leg (v2)
Enable wallet-signed (eth_signTypedData_v4) covenant artifact approvals by
domain-wrapping the approval digest and binding the depositor approval to a
pinned Ethereum identity.
- Wrap the digest: keccak256(0x1901 ‖ domainSeparator ‖ structHash) with an
off-chain, salt-based EIP712Domain(name,version,chainId,salt). chainId+salt are
configured on the signer (Config.EIP712ChainId/EIP712Salt) and threaded through
validation and the tBTC engine so recomputed digests stay consistent.
- Verify the depositor approval against an optional pinned DepositorTrustRoot
ethAddress via ecrecover-and-compare (65-byte sig, v in {27,28}/{0,1}, low-S
enforced); fall back to the secp256k1 script key when unset. Custodian
verification is unchanged.
- Hard cut approvalVersion to 2 (v1 rejected).
- Regenerate approval vectors to _v2; add a real-wallet signature vector whose
digest is cross-verified byte-identical against an independent EIP-712 encoder,
an ecrecover integration test (accept + reject), and a startup guard that fails
loudly when an ethAddress is pinned without a configured chainId.
Refs: keep-core prerequisite for vba-dashboard#172.
keep-core previously implemented only the MIGRATION covenant action. Add the cooperative REDEEM (pay out to a depositor-chosen address) and RENEW (re-lock into the next-epoch covenant) actions end to end. - Add a CovenantAction discriminator (MIGRATION/REDEEM/RENEW) to RouteSubmitRequest (empty defaults to MIGRATION) plus RedeemDestinationReservation and RenewDestinationReservation, alongside the existing migration destination. - Define the redeem/renew destination commitment schemas (SHA-256 over canonical JSON, mirroring the migration commitment) binding the covenant identity to the payout/next-covenant script hash and value. These are the normative schemas the client (covenant-manager / dashboard / verification kit) must mirror. - Dispatch destination validation on the action and reject destinations that do not belong to it; the migration plan-quote path stays MIGRATION-only. Bind the transaction plan's destinationValueSats to the committed output value so the signed output cannot diverge from what the depositor approved. - Branch the transaction builder on the action to pay the migration deposit script, the redeem payout script, or the renew next-covenant script; the input, anchor output, and threshold signing are shared unchanged. Tests cover the commitment schemas, action-dispatched validation (accept/reject), and assert the signed transaction pays exactly the committed redeem/renew output. Refs: keep-core prerequisite for vba-dashboard#172.
…new actions trustRootLookupScope read the scope network only from MigrationDestination, so a wallet-signed REDEEM/RENEW request (which carries a redeem/renew destination, not a migration one) resolved an empty network and failed depositor trust-root matching. Read the network from the destination for the request's action. Add the headline end-to-end test for vba-dashboard#172: a cooperative REDEEM whose depositor artifact approval is signed by a pinned ETH identity is accepted.
|
Important Review skippedAuto reviews are disabled on base/target branches other than the default branch. Please check the settings in the CodeRabbit UI or the ⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Pro Plus Run ID: You can disable this status message by setting the Use the checkbox below for a quick retry:
✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
Address findings from the PR security/simplicity review: - Reject the zero address as a pinned depositor ethAddress so a misconfiguration cannot make ecrecover compare against 0x0. - Reject mixing ethAddress presence across network entries of the same (route, reserve) depositor trust root, closing a silent ETH->secp verification downgrade steered by the request-supplied network value. - Fail closed on unrecognized artifact-approval roles in the authenticity loop instead of silently skipping them. - Correct the defaultArtifactApprovalDomainSalt comment: cross-deployment separation comes from the unique reserve/vault in destinationCommitmentHash plus trust-root scoping, not from scriptTemplateId; recommend per-deployment eip712Salt overrides. - Drop the misleading "predate the field" backward-compatibility note on CovenantAction; v1 approvals are hard-rejected.
Security-review follow-ups (deferred from this PR)The hardening findings addressed in 1. REDEEM/RENEW carry no freshness/authority bound — deferred (needs decision)MIGRATION is gated by an ed25519 Why deferred: the "cryptographically bind and enforce signer-approval expiry" feature was reverted twice recently ( Options: add an expiry field to the approval payload; require a parallel REDEEM/RENEW plan-quote; or explicitly document that epoch-scoped UTXO binding satisfies the threat model and why. 2.
|
Summary
Enables wallet-signed (
eth_signTypedData_v4) covenant artifact approvals and addsthe cooperative REDEEM / RENEW action paths to the keep-core covenant signer.
This is the keep-core prerequisite (section A of the VBA EIP-712 change-list) that must land
before this covenant work goes live: today the signer verifies a bare EIP-712 struct
hash (no
0x1901, no domain separator) against the depositor's secp256k1 key, whichno connected ETH wallet can produce. This PR domain-wraps the digest, binds the depositor
approval to a pinned ETH identity via
ecrecover, and builds out the redeem/renewtransaction path.
Stacked on top of #3882 (
feat/psbt-covenant-final-project-pr) — the only branch carryingpkg/covenantsigner/. Tracked by tlabs-xyz/vba-dashboard#172:https://github.com/tlabs-xyz/vba-dashboard/issues/172
What changed
A1 - Domain-wrap the digest (EIP-712 v2, off-chain / salt-based).
The approval digest is now
keccak256(0x1901 ‖ domainSeparator ‖ structHash)with domainEIP712Domain(string name,string version,uint256 chainId,bytes32 salt)(noverifyingContract, since verification is off-chain).name = "tBTC Covenant Artifact Approval",version = "2",chainId+saltfrom signer config (default salt =keccak256("tBTC Covenant Artifact Approval Domain v2")). Domain params are threaded fromconfig → validation options → digest, including the exported
ComputeArtifactApprovalDigestused by
pkg/tbtc.A2 - ETH identity binding. Optional
EthAddressonDepositorTrustRoot(keyed byroute/reserve/network). When pinned, the depositor approval is verified via
ecrecover-and-compare (65-byter‖s‖v,vin {27,28} or {0,1}, low-S enforced);otherwise it falls back to the secp256k1 script key. Custodian role stays secp256k1.
A3 - Hard cut to
approvalVersion = 2(v1 rejected). Nothing is deployed yet, so notransition window is needed.
A4 - Vectors. Regenerated the v2 approval vectors and added a real-wallet vector
whose digest is cross-verified byte-identical against the independent
eth_account(0.13.7) EIP-712 encoder — genuine
eth_signTypedData_v4compatibility, not aself-referential check. A startup guard rejects a config that pins depositor ETH addresses
while
eip712ChainId == 0.A5 - Cooperative REDEEM / RENEW path. New
CovenantActiondiscriminator(MIGRATION / REDEEM / RENEW; empty defaults to MIGRATION),
RedeemDestinationReservation/RenewDestinationReservation, redeem/renew commitment schemas (SHA-256 over canonical JSON,mirroring migration), action-dispatched validation, and an action branch in
buildCovenantTransactionBuilder. The built transaction is proven to pay exactly thecommitted output — the transaction plan's
destinationValueSatsis bound to the committedoutput value.
Commitment schemas (normative contract for the client repos to mirror):
{reserve, epoch, route, revealer, vault, network, outputScriptHash, outputValueSats}{reserve, epoch, route, revealer, vault, network, nextCovenantScriptHash, nextMaturityHeight, outputValueSats}Client-side contract (must be mirrored byte-for-byte)
keccak256(0x1901 ‖ domainSeparator ‖ structHash).ArtifactApproval(uint8 approvalVersion,bytes32 route,bytes32 scriptTemplateId,bytes32 destinationCommitmentHash,bytes32 planCommitmentHash),approvalVersion = 2.route/scriptTemplateIdarebytes32 = keccak256(identifier string)— the dApp mustpre-hash them; a wallet encoder given the raw bytes32 reproduces the digest.
Verification
go test ./pkg/covenantsigner/...— 156 passedgo test ./pkg/tbtc/...— 275 passed (full suite)go build ./...— green;gofmtcleanaccepted through
Service.Submit, and the signed transaction pays exactly the committedredeem/renew output (scriptPubKey + value).
Out of scope
The section-B/C client sync (covenant-manager, dashboard
engine.ts, verification kit,co-sign service, dual-connection wallet UX) lives in other repos. The pre-existing migration
depositScript-as-output gap is flagged, not fixed here — the redeem/renew paths sidestep itby carrying an explicit output scriptPubKey.