Multi-party (co-attestation)
The multi-party trust layer (Layer 2 in the trust spectrum) lets N independent parties co-sign the same content claim. The SDK collects each party’s EIP-712 signature off-chain, then a coordinator submits one on-chain attestation containing all of them. A separate resolver (FidemarkMultiResolver) atomically recovers each signature on-chain and validates it matches the declared attesters[] array.
The submitter pays gas, but the trust guarantee is in the signatures. Anyone can be the coordinator.
The flow
Section titled “The flow”- Define the claim. All co-signers must sign the same
{ contentHash, contentType, createdAt }triple. - Each co-signer signs. They produce a slip (
{ signer, signature }) and forward it to the coordinator over any transport. - The coordinator submits. One transaction, one UID, all signatures embedded.
- Anyone verifies. The verify response includes
multi.attesters[]andmulti.signatures[]. The chain has already validated them; verifiers can re-check off-chain if they want.
Build the claim
Section titled “Build the claim”Every co-signer must derive the claim from the same inputs.
import { buildMultiPartyClaim } from "@fidemark/sdk";
const claim = buildMultiPartyClaim({ content: theArticle, contentType: "text/article", createdAt: 1735689600, // optional, defaults to now});from fidemark import build_multi_party_claim
claim = build_multi_party_claim( content=the_article, content_type="text/article", created_at=1735689600, # optional)claim := fidemark.BuildMultiPartyClaim([]byte(theArticle), "text/article", 1735689600)The returned claim is { contentHash, contentType, createdAt }. Share it with every signer verbatim.
Sign a slip
Section titled “Sign a slip”import { signMultiPartyClaim, getNetwork } from "@fidemark/sdk";
const network = getNetwork("base-sepolia");const slip = await signMultiPartyClaim(myWallet, claim, network);from fidemark import sign_multi_party_claim, get_network
network = get_network("base-sepolia")slip = sign_multi_party_claim(my_private_key, claim, network)network, _ := fidemark.GetNetwork("base-sepolia")slip, err := fidemark.SignMultiPartyClaim(myPrivateKey, claim, network)The slip is a plain { signer, signature } pair: send it however suits your pipeline (HTTP, file, queue).
Submit the attestation
Section titled “Submit the attestation”The coordinator (anyone with a funded wallet) collects every co-signer’s slip, constructs a Fidemark client signed in their name, and submits the bundle. See Configuration for every constructor option.
import { Fidemark, getNetwork } from "@fidemark/sdk";
const coordinator = new Fidemark({ network: getNetwork("base-sepolia"), privateKey: process.env.COORDINATOR_KEY,});
const result = await coordinator.attestMultiParty({ claim, slips: [aliceSlip, bobSlip, carolSlip],});import osfrom fidemark import ( AttestMultiPartyInput, Fidemark, MultiPartyClaimInput, MultiPartySlipInput, get_network,)
coordinator = Fidemark( network=get_network("base-sepolia"), private_key=os.environ["COORDINATOR_KEY"],)
result = coordinator.attest_multi_party(AttestMultiPartyInput( claim=MultiPartyClaimInput( content_hash=claim.content_hash, content_type=claim.content_type, created_at=claim.created_at, ), slips=[ MultiPartySlipInput(signer=alice_slip.signer, signature=alice_slip.signature), MultiPartySlipInput(signer=bob_slip.signer, signature=bob_slip.signature), MultiPartySlipInput(signer=carol_slip.signer, signature=carol_slip.signature), ],))import ( "context" "log" "os"
"github.com/fidemark/sdk-go/fidemark")
network, _ := fidemark.GetNetwork("base-sepolia")coordinator, err := fidemark.New(fidemark.Config{ Network: network, PrivateKey: os.Getenv("COORDINATOR_KEY"),})if err != nil { log.Fatal(err) }
res, err := coordinator.AttestMultiParty(context.Background(), fidemark.AttestMultiPartyInput{ Claim: claim, Slips: []fidemark.MultiPartySlip{aliceSlip, bobSlip, carolSlip},})The resolver enforces:
2 <= slips.length <= 16signatures[i]recovers toattesters[i]- No duplicate addresses in
attesters[] proofMethodis on the resolver’s allowlist (multi-partyis registered at deploy)createdAtis not too far in the future (1 day drift)
If any check fails, the EAS attest call reverts and no attestation is created.
Verify
Section titled “Verify”const att = await fidemark.verify(result.uid);// att.type === "multi"// att.contentHash === claim.contentHash// att.attester === coordinator address (the submitter)// att.multi.attesters === [alice, bob, carol]// att.multi.signatures === [...EIP-712 sigs]// att.multi.proofMethod === "multi-party"// att.multi.contentType === claim.contentTypeatt = fidemark.verify(result.uid)# att.type === "multi"# att.content_hash == claim.content_hash# att.attester == coordinator address (the submitter)# att.multi.attesters == [alice, bob, carol]# att.multi.signatures == [b"...", b"...", b"..."]# att.multi.proof_method == "multi-party"# att.multi.content_type == claim.content_typeatt, err := client.Verify(ctx, res.UID)// att.Type == fidemark.SchemaMulti// att.ContentHash == claim.ContentHash// att.Attester == coordinator address (the submitter)// att.Multi.Attesters == []string{alice, bob, carol}// att.Multi.Signatures == [][]byte{...}// att.Multi.ProofMethod == "multi-party"// att.Multi.ContentType == claim.ContentTypeTo re-verify a slip off-chain (e.g. for a custom auditor UI), reconstruct the digest and recover the signer.
import { multiPartyClaimDigest } from "@fidemark/sdk";import { recoverAddress } from "ethers";
const digest = multiPartyClaimDigest(claim, network);const recovered = recoverAddress(digest, slip.signature);// recovered === slip.signerfrom fidemark import multi_party_claim_digest
digest = multi_party_claim_digest(claim, network)# Recover via eth_account.Account.recover_message against the same typed data,# or compare the digest to slip.signature off-band.digest, err := fidemark.MultiPartyClaimDigest(claim, network)recovered, err := fidemark.RecoverMultiPartySigner(claim, slip, network)// recovered == slip.SignerThe on-chain resolver also exposes a claimDigest(...) view returning the same value, so you can cross-check from a block explorer.
Use cases
Section titled “Use cases”- Co-authored content: newsroom, research lab, joint statement. Each author signs once.
- Witness verification: a creator signs, then a third-party witness counter-signs. Auditors check both.
- Compliance flows: provenance where multiple stakeholders must approve.
- Threshold attestations: verifiers reading the chain can require N-of-M (e.g. “at least 3 of these 5 attesters must be present”).
Gas profile
Section titled “Gas profile”ECDSA recovery is ~3 000 gas per signature. A 5-party attestation costs roughly the same as a single Human Proof plus 15 000 gas for the recovery loop. At Base mainnet gas prices, that’s well under one cent.
Comparison with attestHuman referencing
Section titled “Comparison with attestHuman referencing”You can approximate multi-party by chaining Human attestations with refUID. Multi-party has three concrete advantages:
- Atomic: all signatures land or none do.
- Cheaper: one tx, one storage cost.
- Verifier-friendly: one UID with a list of co-signers, not a chain to walk.
Use chained Human attestations when the additional signers come in over time (review -> revision -> approval). Use multi-party when the signers are simultaneous.